using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using FwkLight.CodeExtensions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.SqlCommand;

namespace FwkLight.Domain.Fetching
{
  public class MultiCriteriaFetcher<EntityType>: IMultiCriteriaFetcher<EntityType>
  {
    protected IFetchingPart<EntityType> _rootFetchingPart;
    private readonly EntityAliasParser _entityAliasParser;
    
    public int ChangesCount
    {
      get
      {
        if (_rootFetchingPart == null)
          return 0;
        return _rootFetchingPart.ChangesCount;
      }
    }

    public MultiCriteriaFetcher()
    {
      _entityAliasParser = new EntityAliasParser();
    }

    public IFetchingPart<EntityType> InitFetchingHierarchy(string rootAlias)
    {
      if (_rootFetchingPart == null)
        _rootFetchingPart = new FetchingPart<EntityType>(null, rootAlias, typeof(EntityType), "", false, null, true, false);
      return _rootFetchingPart;
    }

    public IFetchingPart<EntityType> InitFetchingHierarchy()
    {
      if (_rootFetchingPart == null)
        _rootFetchingPart = new FetchingPart<EntityType>(null, _entityAliasParser.ParseAliasFor(typeof(EntityType), null), typeof(EntityType), "", false, null, true, false);
      return _rootFetchingPart;
    }

    public IWaitForFilter<EntityType, R> Where<R>(Expression<Func<EntityType, R>> property)
    {
      return InitWaitingFilter(property);
    }

    private IWaitForFilter<EntityType, R> InitWaitingFilter<R>(Expression<Func<EntityType, R>> property)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      string propertyName = ((MemberExpression)property.Body).Member.Name;
      return InitWaitingFilter<R>(propertyName);
    }

    private IWaitForFilter<EntityType, R> InitWaitingFilter<R> ( string propertyName)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();
      
      return new WaitForFilter<EntityType, R>(propertyName, this, _rootFetchingPart);
    }

    public IWaitForFilter<EntityType, R> And<R>(Expression<Func<EntityType, R>> property)
    {
      return InitWaitingFilter(property);
    }

    public IWaitForFilter<EntityType, R> And<R> ( string propertyName)
    {
      return InitWaitingFilter<R>(propertyName);
    }

    public IMultiCriteriaFetcher<EntityType> OrderAscBy<R>(Expression<Func<EntityType, R>> property)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();
      
      string propertyName = ((MemberExpression)property.Body).Member.Name;
      _rootFetchingPart.SortingList.Add(new SortingPart(propertyName, Sorting.Asc, _rootFetchingPart));

      return this;
    }

    public IMultiCriteriaFetcher<EntityType> OrderDescBy<R>(Expression<Func<EntityType, R>> property)
    {
      string propertyName = ((MemberExpression)property.Body).Member.Name;
      _rootFetchingPart.SortingList.Add(new SortingPart(propertyName, Sorting.Desc, _rootFetchingPart));

      return this;
    }

    public void ResetChangesCount()
    {
      _rootFetchingPart.ResetChangesCount();
    }

    protected IList LoadDefinedList(ISession session)
    {
      if (_rootFetchingPart == null)
        throw new NotImplementedException("You must configure at least some filters with FilterBy, and maybe choose a fetching strategy!");

      IList<DetachedCriteria> detachedCriterias = new List<DetachedCriteria>();
      DetachedCriteria currentCriteria = DetachedCriteria.For(_rootFetchingPart.PartType, _rootFetchingPart.Alias);
      detachedCriterias.Add(currentCriteria);

      _rootFetchingPart.Filters.ForEach(filter => filter.ApplyFilterOn(currentCriteria));
      _rootFetchingPart.Specifications.ForEach(spec => spec.EnsureThatWillBeSatisfiedBy(currentCriteria));
			_rootFetchingPart.SpecificationsWithOneParam.ForEach(spec =>
			                                                     	{
																															MethodInfo mi = spec.Specification.GetType().GetMethod("EnsureThatWillBeSatisfiedBy", new[] {typeof(DetachedCriteria), spec.ParamType} );
																															var args = new object[2]; 
																															args[0] = currentCriteria; 
																															args[1] = spec.Param; 
																															mi.Invoke(spec.Specification, args);
			                                                     	});
      _rootFetchingPart.SpecificationsWithTwoParams.ForEach(spec =>
      {
        MethodInfo mi = spec.Specification.GetType().GetMethod("EnsureThatWillBeSatisfiedBy", new[] { typeof(DetachedCriteria), spec.ParamType, spec.Param2Type });
        var args = new object[3];
        args[0] = currentCriteria;
        args[1] = spec.Param;
        args[2] = spec.Param2;
        mi.Invoke(spec.Specification, args);
      });
      _rootFetchingPart.SpecificationsWithTwoParams.ForEach(spec =>
      {
        MethodInfo mi = spec.Specification.GetType().GetMethod("EnsureThatWillBeSatisfiedBy", new[] { typeof(DetachedCriteria), spec.ParamType, spec.Param2Type });
        var args = new object[3];
        args[0] = currentCriteria;
        args[1] = spec.Param;
        args[2] = spec.Param2;
        mi.Invoke(spec.Specification, args);
      });
      _rootFetchingPart.SpecificationsWithThreeParams.ForEach(spec =>
      {
        MethodInfo mi = spec.Specification.GetType().GetMethod("EnsureThatWillBeSatisfiedBy", new[] { typeof(DetachedCriteria), spec.ParamType, spec.Param2Type, spec.Param3Type });
        var args = new object[4];
        args[0] = currentCriteria;
        args[1] = spec.Param;
        args[2] = spec.Param2;
        args[3] = spec.Param3;
        mi.Invoke(spec.Specification, args);
      });
      _rootFetchingPart.SpecificationsWithFourParams.ForEach(spec =>
      {
        MethodInfo mi = spec.Specification.GetType().GetMethod("EnsureThatWillBeSatisfiedBy", new[] { typeof(DetachedCriteria), spec.ParamType, spec.Param2Type, spec.Param3Type, spec.Param4Type });
        var args = new object[5];
        args[0] = currentCriteria;
        args[1] = spec.Param;
        args[2] = spec.Param2;
        args[3] = spec.Param3;
        args[4] = spec.Param4;
        mi.Invoke(spec.Specification, args);
      });
      _rootFetchingPart.SortingList.ForEach(p => p.ApplySortingOn(currentCriteria));

      AttachFetchingPartToCriteria(_rootFetchingPart, currentCriteria, detachedCriterias);

      IMultiCriteria criteria = session.CreateMultiCriteria();
      detachedCriterias.ForEach(p => criteria.Add(p));
      var result = criteria.List();
      IList list = (IList)result[0];

      _rootFetchingPart = null;

      return list;
    }

    private void AttachFetchingPartToCriteria(IFetchingPart fetchingPart, DetachedCriteria currentCriteria, IList<DetachedCriteria> detachedCriterias)
    {
      if (fetchingPart == null)
        return;

      if (!fetchingPart.IsRoot && currentCriteria != null)
      {
        currentCriteria.CreateCriteria(fetchingPart.Parent.Alias + "." + fetchingPart.PropertyName, fetchingPart.Alias, JoinType.LeftOuterJoin);
        fetchingPart.Filters.ForEach(filter => filter.ApplyFilterOn(currentCriteria));
        //fetchingPart.SortingList.ForEach(p => p.ApplySortingOn(currentCriteria));

        if (fetchingPart.IsList)
        {
          var otherEntityChildrenWithFilters =
            fetchingPart.Parent.EntityChildren.Where(p => !p.Equals(fetchingPart) && p.Filters.Count > 0);
          if (otherEntityChildrenWithFilters.Count() > 0)
          {
            otherEntityChildrenWithFilters.ForEach(p =>
                                               {
                                                 currentCriteria.CreateCriteria(
                                                   p.Parent.Alias + "." + p.PropertyName,
                                                   p.Alias, JoinType.LeftOuterJoin);
                                                 p.Filters.ForEach(
                                                   filter => filter.ApplyFilterOn(currentCriteria));
                                                 //p.SortingList.ForEach(u => u.ApplySortingOn(currentCriteria));
                                               });
          }
        }
      }

      fetchingPart.EntityChildren.ForEach(entityChild => AttachFetchingPartToCriteria(entityChild, currentCriteria, detachedCriterias));

      fetchingPart.ListChildren.ForEach(listChild =>
      {
        if (!fetchingPart.IsInverse)
        {
          DetachedCriteria newCriteria =
            DetachedCriteria.For(fetchingPart.PartType, fetchingPart.Alias);
          detachedCriterias.Add(newCriteria);
          AttachFetchingPartToCriteria(listChild, newCriteria, detachedCriterias);

          fetchingPart.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));
          if (fetchingPart.IsRoot)
            _rootFetchingPart.Specifications.ForEach(spec => spec.EnsureThatWillBeSatisfiedBy(newCriteria));
          //fetchingPart.SortingList.ForEach(p => p.ApplySortingOn(newCriteria));

          AddParentCriteriasAndFilters(fetchingPart, newCriteria);
        }
        else
        {
          IFetchingPart parent = fetchingPart.Parent;
          //if (!parent.IsRoot)
          //  throw new NotImplementedException("Reverse associations are not implemented for lower levels than root!");

          if (parent.IsRoot)
          {
            DetachedCriteria newCriteria = DetachedCriteria.For(parent.PartType, parent.Alias);
            detachedCriterias.Add(newCriteria);
            newCriteria.CreateCriteria(parent.Alias + "." + fetchingPart.ParentPropertyName, fetchingPart.Alias,
                                       JoinType.LeftOuterJoin);
            fetchingPart.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));
            parent.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));
            AttachFetchingPartToCriteria(listChild, newCriteria, detachedCriterias);
          }
          else if (parent.Parent.IsRoot)
          {
            IFetchingPart parentOfParent = parent.Parent;
            DetachedCriteria newCriteria = DetachedCriteria.For(parentOfParent.PartType, parentOfParent.Alias);
            detachedCriterias.Add(newCriteria);

            newCriteria.CreateCriteria(parentOfParent.Alias + "." + parent.PropertyName, parent.Alias, JoinType.LeftOuterJoin);
            parentOfParent.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));

            newCriteria.CreateCriteria(parent.Alias + "." + fetchingPart.ParentPropertyName, fetchingPart.Alias,
                                       JoinType.LeftOuterJoin);
            fetchingPart.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));
            parent.Filters.ForEach(filter => filter.ApplyFilterOn(newCriteria));
            AttachFetchingPartToCriteria(listChild, newCriteria, detachedCriterias);
          }
          else throw new NotImplementedException("Reverse associations are not implemented below the second level!");
        }
      });
    }

    private void AddParentCriteriasAndFilters(IFetchingPart fetchingPart, DetachedCriteria criteria)
    {
      IFetchingPart parent = fetchingPart.Parent;
      string parentPropertyName = fetchingPart.ParentPropertyName;
      if (parent == null || parentPropertyName.IsEmpty())
        return;

      criteria.CreateCriteria(fetchingPart.Alias + "." + parentPropertyName, parent.Alias, JoinType.LeftOuterJoin);
      parent.Filters.ForEach(filter => filter.ApplyFilterOn(criteria));
      //parent.SortingList.ForEach(p => p.ApplySortingOn(criteria));

      var childrenWithFilters = parent.EntityChildren.Where(p => p.Filters.Count > 0);
      if (childrenWithFilters.Count() > 0)
      {
        childrenWithFilters.ForEach(p =>
                                      {
                                        criteria.CreateCriteria(
                                          p.Parent.Alias + "." + p.PropertyName,
                                          p.Alias, JoinType.LeftOuterJoin);
                                        p.Filters.ForEach(
                                          filter => filter.ApplyFilterOn(criteria));
                                        //p.SortingList.ForEach(u => u.ApplySortingOn(criteria));
                                      });
      }

      if (parent.IsRoot)
        _rootFetchingPart.Specifications.ForEach(spec => spec.EnsureThatWillBeSatisfiedBy(criteria));

      AddParentCriteriasAndFilters(parent, criteria);
    }

    public IMultiCriteriaFetcher<EntityType> FilterComponentBy<R, M>(Expression<Func<EntityType, R>> component, Expression<Func<R, M>> property, M value)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      string propertyName = ((MemberExpression)component.Body).Member.Name + "." + ((MemberExpression)property.Body).Member.Name;
      _rootFetchingPart.Filters.Add(new FetchingFilterEq(propertyName, value, _rootFetchingPart));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> FilterBy<R, M>(Expression<Func<EntityType, R>> property, Expression<Func<R, M>> propertyIdentifier, M value)
    {
      //if (value == null)
      //  return this;

      //string className = ((MemberExpression)property.Body).Member.Name;
      //string propertyToFilterOn = ((MemberExpression)propertyIdentifier.Body).Member.Name;

      //if(_aliasesAllreadyAdded.Contains(className))
      //{
      //  _criteria.Add(Restrictions.Eq(className + "." + propertyToFilterOn, value));
      //  return this;
      //}

      //_aliasesAllreadyAdded.Add(className);

      //_criteria
      //  .CreateCriteria(className, className)
      //  .Add(Restrictions.Eq(className + "." + propertyToFilterOn, value));

      //return this;

      if (_rootFetchingPart == null)
        throw new NotImplementedException("You can't filter the second level without configuring a fetching strategy!");

      string propertyName = ((MemberExpression)property.Body).Member.Name;
      string propertyIdentifierName = ((MemberExpression)propertyIdentifier.Body).Member.Name;

      IFetchingPart secondLevelFetchingPart =
        _rootFetchingPart.Children.FirstOrDefault(p => p.PropertyName == propertyName);

      if (secondLevelFetchingPart == null)
        throw new NotImplementedException("You need to use a fetching strategy which brings " + propertyName + ", in order to apply filters on it!");

      secondLevelFetchingPart.Filters.Add(new FetchingFilterEq(propertyIdentifierName, value, secondLevelFetchingPart));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> FilterBy<R>(Expression<Func<EntityType, R>> property, R value)
    {
      //if (value == null)
      //  return this;

      //_criteria
      //  .Add(Restrictions.Eq(((MemberExpression)property.Body).Member.Name, value));

      //return this;

      if (_rootFetchingPart == null)
        InitDefaultRoot();

      string propertyName = ((MemberExpression)property.Body).Member.Name;
      _rootFetchingPart.Filters.Add(new FetchingFilterEq(propertyName, value, _rootFetchingPart));
      return this;
    }

    private void InitDefaultRoot()
    {
      string rootAlias = _entityAliasParser.ParseAliasFor(typeof(EntityType), null);
      _rootFetchingPart = new FetchingPart<EntityType>(null, rootAlias, typeof(EntityType), "", false, "", true, false);
    }

    public IMultiCriteriaFetcher<EntityType> FilterBy(IFullSpecification<EntityType> spec)
    {
      //_criteria
      //  .EnsureItWillSatisfy(spec);

      //return this;

      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.Specifications.Add(spec);
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> WhereRootIs(IFullSpecification<EntityType> spec)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.Specifications.Add(spec);
      return this;
    }

		public IMultiCriteriaFetcher<EntityType> WhereRootIs<Param1>(IFullSpecification<EntityType, Param1> spec, Param1 param1)
		{
			if (_rootFetchingPart == null)
				InitDefaultRoot();

			_rootFetchingPart.SpecificationsWithOneParam.Add(new SpecificationWithOneParam(spec, param1));
			return this;
		}

    public IMultiCriteriaFetcher<EntityType> WhereRootIs<Param1, Param2>(IFullSpecification<EntityType, Param1, Param2> spec, Param1 param1, Param2 param2)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithTwoParams.Add(new SpecificationWithTwoParams(spec, param1, param2));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> WhereRootIs<Param1, Param2, Param3>(IFullSpecification<EntityType, Param1, Param2, Param3> spec, Param1 param1, Param2 param2, Param3 param3)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithThreeParams.Add(new SpecificationWithThreeParams(spec, param1, param2, param3));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> WhereRootIs<Param1, Param2, Param3, Param4>(IFullSpecification<EntityType, Param1, Param2, Param3, Param4> spec, Param1 param1, Param2 param2, Param3 param3, Param4 param4)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithFourParams.Add(new SpecificationWithFourParams(spec, param1, param2, param3, param4));
      return this;
    }

		public IMultiCriteriaFetcher<EntityType> WhereRootCanBe(IFullSpecification<EntityType> spec)
		{
			return WhereRootIs(spec);
		}

		public IMultiCriteriaFetcher<EntityType> WhereRootCanBe<Param1>(IFullSpecification<EntityType, Param1> spec, Param1 param1)
		{
			return WhereRootIs(spec, param1);
		}

    public IMultiCriteriaFetcher<EntityType> WhereRootCanBe<Param1, Param2>(IFullSpecification<EntityType, Param1, Param2> spec, Param1 param1, Param2 param2)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithTwoParams.Add(new SpecificationWithTwoParams(spec, param1, param2));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> WhereRootCanBe<Param1, Param2, Param3>(IFullSpecification<EntityType, Param1, Param2, Param3> spec, Param1 param1, Param2 param2, Param3 param3)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithThreeParams.Add(new SpecificationWithThreeParams(spec, param1, param2, param3));
      return this;
    }

    public IMultiCriteriaFetcher<EntityType> WhereRootCanBe<Param1, Param2, Param3, Param4>(IFullSpecification<EntityType, Param1, Param2, Param3, Param4> spec, Param1 param1, Param2 param2, Param3 param3, Param4 param4)
    {
      if (_rootFetchingPart == null)
        InitDefaultRoot();

      _rootFetchingPart.SpecificationsWithFourParams.Add(new SpecificationWithFourParams(spec, param1, param2, param3, param4));
      return this;
    }
  }

	public class SpecificationWithOneParam
	{
		public SpecificationWithOneParam(object specification, object param)
		{
			Specification = specification;
			Param = param;
			ParamType = param.GetType();
		}

		public object Specification { get; protected set; }
		public object Param { get; protected set; }
		public Type ParamType { get; protected set; }
	}

  public class SpecificationWithTwoParams
  {
    public SpecificationWithTwoParams(object specification, object param, object param2)
    {
      Specification = specification;
      Param = param;
      Param2 = param2;
      ParamType = param.GetType();
      Param2Type = param2.GetType();
    }

    public object Specification { get; protected set; }
    public object Param { get; protected set; }
    public Type ParamType { get; protected set; }
    public object Param2 { get; protected set; }
    public Type Param2Type { get; protected set; }
  }

  public class SpecificationWithThreeParams
  {
    public SpecificationWithThreeParams(object specification, object param, object param2, object param3)
    {
      Specification = specification;
      Param = param;
      Param2 = param2;
      Param3 = param3;
      ParamType = param.GetType();
      Param2Type = param2.GetType();
      Param3Type = param3.GetType();
    }

    public object Specification { get; protected set; }
    public object Param { get; protected set; }
    public Type ParamType { get; protected set; }
    public object Param2 { get; protected set; }
    public Type Param2Type { get; protected set; }
    public object Param3 { get; protected set; }
    public Type Param3Type { get; protected set; }
  }

  public class SpecificationWithFourParams
  {
    public SpecificationWithFourParams(object specification, object param, object param2, object param3, object param4)
    {
      Specification = specification;
      Param = param;
      Param2 = param2;
      Param3 = param3;
      Param4 = param4;
      ParamType = param.GetType();
      Param2Type = param2.GetType();
      Param3Type = param3.GetType();
      Param4Type = param4.GetType();
    }

    public object Specification { get; protected set; }
    public object Param { get; protected set; }
    public Type ParamType { get; protected set; }
    public object Param2 { get; protected set; }
    public Type Param2Type { get; protected set; }
    public object Param3 { get; protected set; }
    public Type Param3Type { get; protected set; }
    public object Param4 { get; protected set; }
    public Type Param4Type { get; protected set; }
  }
}